package io.hellsinger.vortex.ui.screen.storage.list

import android.os.Bundle
import io.hellsinger.filesystem.attribute.DefaultPathAttribute
import io.hellsinger.filesystem.attribute.PathType
import io.hellsinger.filesystem.linux.LinuxOperationOptions.Access
import io.hellsinger.filesystem.linux.LinuxOperationOptions.Observe
import io.hellsinger.filesystem.linux.attribute.attributes
import io.hellsinger.filesystem.linux.directory.LinuxDirectoryEntry
import io.hellsinger.filesystem.linux.operation.LinuxPathObserver
import io.hellsinger.filesystem.path.Path
import io.hellsinger.viewmodel.BitViewModel
import io.hellsinger.viewmodel.android.SavableViewModel
import io.hellsinger.viewmodel.intent
import io.hellsinger.vortex.Dispatchers
import io.hellsinger.vortex.bit.Bits
import io.hellsinger.vortex.data.model.OperationState
import io.hellsinger.vortex.data.model.PathItem
import io.hellsinger.vortex.data.model.name
import io.hellsinger.vortex.data.repository.AndroidPermissions
import io.hellsinger.vortex.data.repository.AndroidRestrictedStorageRepository
import io.hellsinger.vortex.data.repository.AndroidStorageRepository
import io.hellsinger.vortex.data.repository.StorageOperationStateConsumer
import io.hellsinger.vortex.domain.repository.PathObservation
import io.hellsinger.vortex.domain.repository.StorageRepository
import io.hellsinger.vortex.foundation.bundle.parcelable
import io.hellsinger.vortex.foundation.isAtLeastAndroid11
import io.hellsinger.vortex.service.storage.transaction.TransactionTypes
import io.hellsinger.vortex.util.PathItemComparators
import io.hellsinger.vortex.util.StoragePathSegmentParser
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch

class StorageListScreenViewModel(
    private val repository: StorageRepository,
    private val permissions: AndroidPermissions,
    private val segment: StoragePathSegmentParser,
    private val observation: PathObservation,
    private val dispatchers: Dispatchers,
    private val consumer: StorageOperationStateConsumer,
    private val restricted: AndroidRestrictedStorageRepository,
) : BitViewModel<UiState>(scopeName = "Storage List Scope"),
    Bits,
    SavableViewModel {
    private var itemStateChangeListener: ItemStateChangeListener? = null
    private var fileOperationProgressListener: FileOperationProgressListener? = null

    private var comparator: Int = NAME_COMPARATOR
    private var filter: Int = NO_FILTER

    private val jobs = mutableListOf<Job>()

    private var state = MutableUiState()

    override fun getUiState(): UiState = state

    fun bindItemStateChangeListener(listener: ItemStateChangeListener) {
        itemStateChangeListener = listener
    }

    fun unbindItemStateChangeListener() {
        itemStateChangeListener = null
    }

    fun bindFileOperationProgressListener(listener: FileOperationProgressListener) {
        fileOperationProgressListener = listener
    }

    fun unbindFileOperationProgressListener() {
        fileOperationProgressListener = null
    }

    fun collectPathEvents() {
        jobs +=
            observation.events
                .filterIsInstance<LinuxPathObserver.LinuxObservablePath<Path>>()
                .onEach { (event, path, cookie) ->
                    val isDirectory = event hasFlag Observe.Attributes.DIR
                    if (event hasFlag Observe.CREATE) {
                        return@onEach onPathAdded(
                            path,
                            cookie,
                            isDirectory,
                        )
                    }
                    if (event hasFlag Observe.MODIFY) {
                        return@onEach onPathModified(
                            path,
                            cookie,
                            isDirectory,
                        )
                    }
                    if (event.hasFlags(Observe.DELETE, Observe.DELETE_SELF)) {
                        return@onEach onPathRemoved(
                            path,
                            cookie,
                            isDirectory,
                        )
                    }
                }.launchIn(viewModelScope)

        jobs +=
            consumer
                .getState()
                .onEach { state ->

                    when (state.type) {
                        TransactionTypes.MOVE -> {
                            val transition = state as OperationState.Transition
                            val message =
                                "${transition.source.name} moved to ${transition.dest.name}"
                        }
                    }
                }.launchIn(viewModelScope)
    }

    fun checkPermission() =
        intent {
            if (isAtLeastAndroid11) {
                if (permissions.requiresFullStorageAccess()) {
                    return@intent update(UPDATE_MESSAGE addFlag REASON_STORAGE_FULL_PERMISSION)
                }
            } else {
                if (permissions.requiresReadPermission()) {
                    return@intent update(UPDATE_MESSAGE addFlag REASON_STORAGE_PERMISSION)
                }
            }

            navigate(
                state.selectedTrailItem ?: PathItem(
                    AndroidStorageRepository.EXTERNAL_STORAGE_PATH,
                    DefaultPathAttribute(type = PathType.Directory),
                ),
            )
        }

    fun navigateFromTrail(position: Int) = navigate(getUiState().trail[position])

    fun hasPendingTasks() = repository.getPendingTransitions().value.isNotEmpty()

    fun navigate(item: PathItem) =
        intent {
            if (!item.isPathAbsolute) return@intent
            if (item == getUiState().selectedTrailItem) return@intent // updateCurrent()

            val segments = segment.parse(item)
            val selectedIndex = segments.lastIndex

            var hasPrefix = true
            for (index in segments.indices) {
                if (hasPrefix && index < state.trail.size) {
                    if (state.trail[index] != segments[index]) {
                        hasPrefix = false
                    }
                }
            }

            if (hasPrefix) for (index in segments.size until state.trail.size) segments.add(state.trail[index])

            state.trail.clear()
            state.trail.addAll(segments)
            state.selectedTrailItemPosition = selectedIndex

            update(UPDATE_TRAIL_LIST)

            val progress =
                launch {
                    delay(1000L)
                    update(UPDATE_LOADING_PROGRESS)
                }

            if (isAtLeastAndroid11) {
                if (restricted.isRestricted(item.path)) {
                    progress.cancel()
//                         Since Android 13 fixed feature with SAF, we don't want to support restricted dirs
//                         Besides, Thanks Google (sarcastic)
                    update(UPDATE_MESSAGE addFlag REASON_RESTRICTED_ITEM)
                    return@intent
                }
            }

            state.directoriesCount = 0
            state.filesCount = 0

            val checkResult =
                repository.check(path = item.path, flags = Access.READABLE or Access.EXISTENCE)

            if (checkResult != 0) {
                when {
                    checkResult hasFlag Access.READABLE -> {
                        progress.cancel()
                        update(UPDATE_MESSAGE addFlag REASON_ITEM_IS_NOT_READABLE)
                        return@intent
                    }

                    checkResult hasFlag Access.EXISTENCE -> {
                        progress.cancel()
                        update(UPDATE_MESSAGE addFlag REASON_ITEM_DOES_NOT_EXIST)
                        return@intent
                    }
                }
            }

            observe(item)

            var items = prepareItems(item)

            progress.cancel()

            if (items.isEmpty()) {
                update(UPDATE_TRAIL_LIST addFlag UPDATE_MESSAGE addFlag REASON_EMPTY_ITEMS)
                return@intent
            }

            items = items.sortedWith(getComparator())
            state.replace(items)

            update(UPDATE_LIST)
        }

    fun updateCurrent() =
        intent {
            val current = getUiState().selectedTrailItem ?: return@intent

            if (isAtLeastAndroid11) {
                if (restricted.isRestricted(current.path)) {
                    // Since Android 13 fixed feature with SAF, we don't want to support restricted dirs
                    // Besides, Thanks Google (sarcastic)
                    update(UPDATE_MESSAGE addFlag REASON_RESTRICTED_ITEM)
                    return@intent
                }
            }

            state.directoriesCount = 0
            state.filesCount = 0

            val items = prepareItems(current)

            if (items.isEmpty()) {
                update(UPDATE_TRAIL_LIST addFlag UPDATE_MESSAGE addFlag REASON_EMPTY_ITEMS)
                return@intent
            }

            if (!items.containsAll(state.items)) {
                state.replace(items.sortedWith(getComparator()))
                update(UPDATE_LIST)
            }
        }

    fun navigateBack() =
        intent {
            state.selectedTrailItemPosition--

            state.directoriesCount = 0
            state.filesCount = 0

            val progress =
                launch {
                    delay(1000L)
                    update(UPDATE_LOADING_PROGRESS)
                }

            val item = state.trail[state.selectedTrailItemPosition]

            var items = prepareItems(item)

            progress.cancel()

            if (items.isEmpty()) {
                update(UPDATE_TRAIL_SELECTED_CHANGED addFlag UPDATE_MESSAGE addFlag REASON_EMPTY_ITEMS)
                return@intent
            }

            items = items.sortedWith(getComparator())

            state.replace(items)

            update(UPDATE_TRAIL_SELECTED_CHANGED addFlag UPDATE_LIST)
        }

    private suspend fun prepareItems(directory: PathItem): List<PathItem> {
        try {
            var flow =
                repository
                    .getEntries(directory.path)
                    .filterIsInstance<LinuxDirectoryEntry<Path>>()
                    .map { entry ->
                        when (entry.type) {
                            PathType.Directory -> state.directoriesCount++
                            PathType.File -> state.filesCount++
                        }
                        PathItem(
                            entry.path,
                            DefaultPathAttribute(id = entry.id, type = entry.type),
                        )
                    }
            when (filter) {
                ONLY_DIRECTORIES_FILTER -> flow = flow.filter { item -> item.isDirectory }
                ONLY_FILES_FILTER -> flow = flow.filter { item -> item.isFile }
            }
            return flow.flowOn(dispatchers.io).toList()
        } catch (_: IllegalArgumentException) {
            return emptyList()
        }
    }

    fun isSelected(position: Int): Boolean = isSelected(getUiState().items[position])

    fun isSelected(item: PathItem): Boolean = getUiState().selected.contains(item)

    fun toggleSelection(position: Int) =
        intent {
            val item = getUiState().items[position]

            if (getUiState().selected.contains(item)) {
                state.selected.remove(item)
                itemStateChangeListener?.onDeselect(position)
            } else {
                state.selected.add(item)
                itemStateChangeListener?.onSelect(position)
            }

            update(UPDATE_SELECTION_COUNT)
        }

    fun select(position: Int) =
        intent {
            val item = state.items[position]
            if (getUiState().selected.contains(item)) return@intent

            state.selected.add(item)
            itemStateChangeListener?.onSelect(position)
            update(UPDATE_SELECTION_COUNT)
        }

    fun deselect(item: PathItem) =
        intent {
            if (item !in state.selected) return@intent

            state.selected.remove(item)
            update(UPDATE_SELECTION_COUNT)
        }

    fun deselect(position: Int) =
        intent {
            val item = state.items[position]
            if (!getUiState().selected.contains(item)) return@intent

            state.selected.remove(item)
            update(UPDATE_SELECTION_COUNT)
        }

    fun clearSelection() =
        intent {
            if (getUiState().selected.isEmpty()) return@intent

            state.selected.clear()
            update(UPDATE_SELECTION_COUNT)
        }

    private fun onPathRemoved(
        path: Path,
        cookie: Int,
        isDirectory: Boolean,
    ) = intent {
        if (path == getUiState().selectedTrailItem?.path) {
            for (i in state.selectedTrailItemPosition until state.trail.size) {
                state.trail.removeAt(i)
            }
            state.selectedTrailItemPosition--
            update(UPDATE_TRAIL_LIST)
            return@intent excludedTrailNavigation()
        }

        val ti = getUiState().trail.indexOfFirst { item -> item.path == path }

        // Got it: Monitored path present in trail
        // Remove parent and children with selection checking
        if (ti >= 0) {
            if (ti <= state.selectedTrailItemPosition) {
                state.selectedTrailItemPosition = ti - 1
                for (i in state.selectedTrailItemPosition until state.trail.size) {
                    state.trail.removeAt(i)
                }
                state.selectedTrailItemPosition--
                update(UPDATE_TRAIL_LIST)
                return@intent excludedTrailNavigation()
            }

            for (i in ti until state.trail.size) {
                state.trail.removeAt(i)
            }
            update(UPDATE_TRAIL_LIST)
        }

        val li = getUiState().items.indexOfFirst { item -> item.path == path }

        // Got it: Monitored path present in list
        // Remove parent
        if (li >= 0) {
            val item = state.items.removeAt(li)
            deselect(item)
            if (isDirectory) {
                state.directoriesCount--
            } else {
                state.filesCount--
            }

            if (state.items.isEmpty()) {
                update(UPDATE_TRAIL_SELECTED_CHANGED addFlag UPDATE_MESSAGE addFlag REASON_EMPTY_ITEMS)
                return@intent
            }

            update(UPDATE_LIST)
        }
    }

    private fun excludedTrailNavigation() =
        intent {
            state.directoriesCount = 0
            state.filesCount = 0

            val progress =
                launch {
                    delay(1000L)
                    update(UPDATE_LOADING_PROGRESS)
                }

            val item = state.trail[state.selectedTrailItemPosition]
            var items = prepareItems(item)

            progress.cancel()

            if (items.isEmpty()) {
                update(UPDATE_TRAIL_SELECTED_CHANGED addFlag UPDATE_MESSAGE addFlag REASON_EMPTY_ITEMS)
                return@intent
            }

            items = items.sortedWith(getComparator())

            state.replace(items)

            update(UPDATE_TRAIL_SELECTED_CHANGED addFlag UPDATE_LIST)
        }

    private fun onPathAdded(
        path: Path,
        cookie: Int,
        isDirectory: Boolean,
    ) = intent {
        if (path.parent != getUiState().selectedTrailItem?.path) return@intent

        val item = PathItem(path, path.attributes)

        if (item in state.items) return@intent

        if (isDirectory) {
            state.directoriesCount++
        } else {
            state.filesCount++
        }

        state.items.add(item)
        state.replace(state.items.sortedWith(getComparator()))
        update(UPDATE_LIST)
    }

    private fun onPathModified(
        path: Path,
        cookie: Int,
        isDirectory: Boolean,
    ) = intent {
        val li = getUiState().items.indexOfFirst { item -> item.path == path }

        if (li >= 0) {
            state.items[li] = PathItem(path)
            update(UPDATE_LIST)
        }
    }

    private fun observe(item: PathItem) {
        if (isCanObserved(item)) {
            observation.addPath(
                item.path,
                Observe.Default,
            )
        }
    }

    private fun isCanObserved(item: PathItem): Boolean =
        if (isAtLeastAndroid11) {
            !restricted.isRestricted(item.path)
        } else {
            true
        }

    override fun onDestroy() {
        super.onDestroy()
        observation.removeAll()
        unbindItemStateChangeListener()
        unbindFileOperationProgressListener()
    }

    fun applyFilter(filter: Int) {
        this.filter = this.filter toggleFlag filter
        updateCurrent()
    }

    fun applyComparator(comparator: Int) {
        this.comparator = comparator
        updateCurrent()
    }

    override fun restore(
        buffer: Bundle,
        prefix: String,
    ): Boolean {
        val state = buffer.parcelable(BUNDLE_KEY, MutableUiState())
        this.state = state
        return true
    }

    override fun save(
        buffer: Bundle,
        prefix: String,
    ): Boolean {
        buffer.putParcelable(BUNDLE_KEY, state)
        return true
    }

    private fun getComparator(): Comparator<PathItem> =
        when (comparator) {
            NAME_COMPARATOR -> PathItemComparators.Name
            PATH_COMPARATOR -> PathItemComparators.Path
            LAST_MODIFIED_TIME_COMPARATOR -> PathItemComparators.LastModifiedTime
            LAST_ACCESS_TIME_COMPARATOR -> PathItemComparators.LastAccessTime
            CREATION_TIME_COMPARATOR -> PathItemComparators.CreationTime
            else -> throw NotImplementedError("Unimplemented version of comparator ($comparator)")
        }

    interface ItemStateChangeListener {
        fun onSelect(position: Int)

        fun onDeselect(position: Int)
    }

    interface FileOperationProgressListener {
        fun handleMessage(
            message: String,
            progress: Long,
            max: Long,
        )
    }

    companion object {
        private const val BUNDLE_KEY = "list_screen_view_model_state"

        const val UPDATE_SCREEN = 0

        const val UPDATE_LIST = 1 shl 0

        const val UPDATE_LOADING_PROGRESS = 1 shl 4

        const val UPDATE_MESSAGE = 1 shl 5

        const val REASON_ITEM_IS_NOT_READABLE = 3 shl 23
        const val REASON_ITEM_DOES_NOT_EXIST = 3 shl 24

        // Flags for UPDATE_MESSAGE, describes what kind
        // Empty list (e.g. directory empty)
        const val REASON_EMPTY_ITEMS = 3 shl 25

        // Restricted directory (Android 11)
        const val REASON_RESTRICTED_ITEM = 3 shl 26
        const val REASON_STORAGE_PERMISSION = 3 shl 27
        const val REASON_STORAGE_FULL_PERMISSION = 3 shl 28

        const val UPDATE_TRAIL_LIST = 1 shl 7
        const val UPDATE_TRAIL_SELECTED_CHANGED = 1 shl 11

        const val UPDATE_SELECTION_COUNT = 1 shl 12

        const val NO_FILTER = 0
        const val ONLY_FILES_FILTER = 1 shl 0
        const val ONLY_DIRECTORIES_FILTER = 1 shl 1

        const val NAME_COMPARATOR = 0
        const val PATH_COMPARATOR = 1 shl 0
        const val LAST_MODIFIED_TIME_COMPARATOR = 1 shl 1
        const val LAST_ACCESS_TIME_COMPARATOR = 1 shl 2
        const val CREATION_TIME_COMPARATOR = 1 shl 3
    }
}
